package com.tau.egonetwork.gui.core.graphs;

import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Frame;
import java.awt.Paint;
import java.awt.Stroke;
import java.awt.event.MouseEvent;
import java.util.Collection;
import java.util.Iterator;
import java.util.Map;

import javax.swing.ToolTipManager;

import org.apache.commons.collections15.Predicate;
import org.apache.commons.collections15.Transformer;
import org.apache.poi.openxml4j.exceptions.InvalidOperationException;
import org.eclipse.jface.layout.GridDataFactory;
import org.eclipse.swt.SWT;
import org.eclipse.swt.awt.SWT_AWT;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.widgets.Button;
import org.eclipse.swt.widgets.Combo;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Event;
import org.eclipse.swt.widgets.Label;
import org.eclipse.swt.widgets.Listener;
import org.eclipse.swt.widgets.TabFolder;
import org.eclipse.swt.widgets.TabItem;

import com.tau.egonetwork.algorithms.EgoNetworkGraph;
import com.tau.egonetwork.algorithms.GraphAdapter;
import com.tau.egonetwork.algorithms.GraphUtils;
import com.tau.egonetwork.common.VisualizationPreferencesFile;
import com.tau.egonetwork.gui.Environment;
import com.tau.egonetwork.gui.core.NumberInputDialog;
import com.tau.egonetwork.profiles.MainProfile;

import edu.uci.ics.jung.algorithms.layout.Layout;
import edu.uci.ics.jung.graph.Graph;
import edu.uci.ics.jung.graph.util.Context;
import edu.uci.ics.jung.visualization.GraphZoomScrollPane;
import edu.uci.ics.jung.visualization.VisualizationViewer;
import edu.uci.ics.jung.visualization.control.CrossoverScalingControl;
import edu.uci.ics.jung.visualization.control.DefaultModalGraphMouse;
import edu.uci.ics.jung.visualization.control.GraphMouseListener;
import edu.uci.ics.jung.visualization.control.ModalGraphMouse;
import edu.uci.ics.jung.visualization.control.ScalingControl;
import edu.uci.ics.jung.visualization.renderers.Renderer.VertexLabel.Position;

public abstract class BasicVisualizeTab<T extends Comparable<T>> {

	protected static final int WIDTH = 700;
	protected static final int HEIGHT = 300;

	private TabItem tab;
	private Display display;
	private boolean displayViewCombo;

	protected Map<T, Integer> currentCluster = null;
	protected T currentVertex = null;
	
	protected EgoNetworkGraph<T> originalGraph = null;
	
	private Combo modeCombo;
	private Combo viewCombo;
	private Combo showClustersCombo;
	private Button onlyClickedVertexButton;
	private Button closeButton;

	public BasicVisualizeTab(TabItem tab, Display display, boolean displayViewCombo) {
		this.tab = tab;
		this.display = display;
		this.displayViewCombo = displayViewCombo;
	}
	
	protected void populateViewCombo(Combo viewCombo) {
		if (displayViewCombo)
			throw new InvalidOperationException("if displayViewCombo is set true, subclass must override this and populate the combo!");
	}
	
	protected Listener getViewComboSelectionListener(VisualizationViewer<T, Integer> viewer) {
		if (displayViewCombo)
			throw new InvalidOperationException("if displayViewCombo is set true, subclass must override this and handle selection listener!");
		return null;
	}
	
	protected abstract Graph<T, Integer> buildGraph(MainProfile<T> profile);
	protected abstract Layout<T, Integer> getLayout(MainProfile<T> profile, Graph<T, Integer> graph);
	protected abstract Transformer<T, Paint> getVertexFillColorTransformer(MainProfile<T> profile);
	protected abstract String getGroupName(MainProfile<T> profile, T id);
	protected abstract Float getHowMuchToScale();
	
	private boolean shouldDisplay(Graph<T, Integer> graph, T vertex) {
		
		if (currentVertex == null)
			return true;
		
		//see if t is currentVertex or its' neighbor:
		boolean flag = false;
		if (!currentVertex.equals(vertex)) {
			for (int edge : graph.getIncidentEdges(vertex)) {
				//for each edge, find the one that's not t:
				T otherVertexOnEdge = null;
				for (T t : graph.getIncidentVertices(edge)) {
					if (!t.equals(vertex)) {
						otherVertexOnEdge = t;
						break;
					}
				}
				if (currentVertex.equals(otherVertexOnEdge)) {
					//we've found that t is currentVertex's neighbor, therefore it
					//should be displayed!
					flag = true;
					break;
				}
			}
		} else {
			flag = true;
		}
		
		return flag;
	}

	private boolean shouldDisplay(Graph<T, Integer> graph, int edge) {
		
		if (currentVertex == null)
			return true;
		
		//see if t is currentVertex or its' neighbor:
		for (T t : graph.getIncidentVertices(edge)) {
			if (t.equals(currentVertex))
				return true;
		}
		return false;
	}
	
	private void buildMenuBar(Composite tabComposite) {
		
		Composite menuBarComposite = new Composite(tabComposite, SWT.NONE);
		menuBarComposite.setLayout(new GridLayout(2, false));
		menuBarComposite.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false));
		menuBarComposite.setBackgroundImage(Environment.BG_TOOLBAR);
		menuBarComposite.setBackgroundMode(SWT.INHERIT_FORCE);
		
		Composite buttonsComposite = new Composite(menuBarComposite, SWT.NONE);
		buttonsComposite.setLayout(new GridLayout(5 + (displayViewCombo ? 2 : 0), false));
		buttonsComposite.setLayoutData(new GridData(SWT.LEFT, SWT.CENTER, true, false));
		
		Label modeLabel = new Label(buttonsComposite, SWT.NONE);
		modeLabel.setLayoutData(new GridData(SWT.LEFT, SWT.CENTER, false, false));
		modeLabel.setText("Mode: ");
		
		modeCombo = new Combo(buttonsComposite, SWT.DROP_DOWN | SWT.READ_ONLY);
		modeCombo.setLayoutData(new GridData(SWT.LEFT, SWT.CENTER, false, false));
		modeCombo.setBackground(Environment.COLOR_WHITE);
		modeCombo.add("Transforming ");
		modeCombo.add("Picking ");
		modeCombo.select(0);
		
		if (displayViewCombo) {
			
			Label viewLabel = new Label(buttonsComposite, SWT.NONE);
			modeCombo.setLayoutData(new GridData(SWT.LEFT, SWT.CENTER, false, false));
			viewLabel.setText("View: ");
			
			viewCombo = new Combo(buttonsComposite, SWT.DROP_DOWN | SWT.READ_ONLY);
			viewCombo.setLayoutData(new GridData(SWT.LEFT, SWT.CENTER, false, false));
			viewCombo.setBackground(Environment.COLOR_WHITE);
			populateViewCombo(viewCombo);
		}
		
		Label showClustersLabel = new Label(buttonsComposite, SWT.NONE);
		showClustersLabel.setText("Show clusters: ");
		
		showClustersCombo = new Combo(buttonsComposite, SWT.DROP_DOWN | SWT.READ_ONLY);
		showClustersCombo.setLayoutData(new GridData(SWT.LEFT, SWT.CENTER, false, false));
		showClustersCombo.setBackground(Environment.COLOR_WHITE);
		showClustersCombo.add("Don't display clusters");
		showClustersCombo.add("Display clusters");
		showClustersCombo.add("Display clusters (calculate without egocenteric node)");
		showClustersCombo.select(0);

		onlyClickedVertexButton = new Button(buttonsComposite, SWT.CHECK);
		onlyClickedVertexButton.setText("Show only clicked vertex");
		onlyClickedVertexButton.setToolTipText("This opention allows you to click on a node and see only its' connections.\n" +
				"The rest will be hidden out of sight. Uncheck this option to return to full view.\n" +
				"Note: This option is only possible when statistics are shown on hover.");
		onlyClickedVertexButton.setEnabled(VisualizationPreferencesFile.getDisplayStatistics().equals(
												VisualizationPreferencesFile.DISPLAY_STATISTICS_HOVER));
		
		closeButton = new Button(menuBarComposite, SWT.PUSH);
		closeButton.setLayoutData(new GridData(SWT.RIGHT, SWT.CENTER, false, false));
		closeButton.setImage(Environment.ICON_CLOSE);
		closeButton.setText("Close Visualization");
		
	}
	
	public void buildTab(final TabFolder parent, final MainProfile<T> profile) {
		
		Composite tabComposite = new Composite(parent, SWT.NONE);
		tabComposite.setLayout(new GridLayout(1, true));
		tabComposite.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true));

		buildMenuBar(tabComposite);
		
		//now for the graph composite itself:
		
		Composite graphComposite = new Composite(tabComposite, SWT.BORDER | SWT.EMBEDDED);
		graphComposite.setLayout(new GridLayout(1, false));
		graphComposite.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true));
				
		//bridge the gap between SWT and AWT (which Jung uses) :
		Frame graphFrame = SWT_AWT.new_Frame(graphComposite);
		GridDataFactory.fillDefaults().grab(true, true).applyTo(graphComposite);
		
		//build the graph:
		final Graph<T, Integer> graph = buildGraph(profile);
		
		//assume originalGraph:
		originalGraph = GraphAdapter.getGraph(profile);

		//set Layout (place vertices where they should be):
		final Layout<T, Integer> layout = getLayout(profile, graph);
		
        //set the visualization viewer and its' components:
        final VisualizationViewer<T, Integer> viewer = new VisualizationViewer<T, Integer>(layout);
        viewer.setDoubleBuffered(true);
        viewer.setPreferredSize(new Dimension(WIDTH, HEIGHT));

        /* ***************************** GRAPH VISUALIZATION PROPERTIES (COLORS AND SUCH) : ***********************/
        
		Transformer<T, String> labelTransformer = new Transformer<T, String>() {

			@Override
			public String transform(T t) {			
				if (t.equals(profile.getID()))
					return profile.getName();
				return profile.getFriendsList().get(t).getName();
			}
		};
		
		Transformer<Integer, Stroke> edgeStrokeTransformer = new Transformer<Integer, Stroke>() {

			@Override
			public Stroke transform(Integer edge) {
								
				if (currentCluster == null)
					return new BasicStroke(1f);
				
				if (!VisualizationPreferencesFile.getShowClusterEdgesAsBold())
					return new BasicStroke(1f);
				
				Collection<T> incidentVertices = graph.getIncidentVertices(edge);
				Iterator<T> iterator = incidentVertices.iterator();
				T v1 = iterator.next();
				T v2 = iterator.next();
				
				if (currentCluster.get(v1) == currentCluster.get(v2) && currentCluster.get(v1) != null) {
					//in the same cluster, make edge bolder:
					return new BasicStroke(2.5f);
				}
				
				//otherwise, return regular stroke:
				return new BasicStroke(1f);
			}
			
		};
		
		Transformer<Integer, Paint> edgeDrawPaintTransformer = new Transformer<Integer, Paint>() {
			
			@Override
			public Paint transform(Integer edge) {
				
				if (currentCluster == null)
					return Color.BLACK;
				
				String preference = VisualizationPreferencesFile.getShowEdgesNotInCluster();
				if (preference.equals(VisualizationPreferencesFile.SHOW_NON_CLUSTER_EDGES_DONT_SHOW))
					return new Color(0, 0, 0, 0);
				
				//otherwise, show it but faded if both vertices not in same cluster:
				
				Collection<T> incidentVertices = graph.getIncidentVertices(edge);
				Iterator<T> iterator = incidentVertices.iterator();
				T v1 = iterator.next();
				T v2 = iterator.next();
				
				if (currentCluster.get(v1) == currentCluster.get(v2) && currentCluster.get(v1) != null) {
					//in the same cluster, color edge normally:
					return Color.BLACK;
				}
				
				//otherwise, return it faded:
				return new Color(0, 0, 0, 100);

			}
		};
		
		Predicate<Context<Graph<T, Integer>, T>> displayVertexPredicate = new Predicate<Context<Graph<T, Integer>, T>>() {

			@Override
			public boolean evaluate(Context<Graph<T, Integer>, T> context) {
				return shouldDisplay(context.graph, context.element);
			}

		};
		
		Predicate<Context<Graph<T, Integer>, Integer>> displayEdgePredicate = new Predicate<Context<Graph<T, Integer>, Integer>>() {

			@Override
			public boolean evaluate(Context<Graph<T, Integer>, Integer> context) {
				return shouldDisplay(context.graph, context.element);
			}
			
		};
		
		viewer.getRenderContext().setVertexLabelTransformer(labelTransformer);
		viewer.getRenderer().getVertexLabelRenderer().setPosition(Position.AUTO);
                 
		viewer.getRenderContext().setVertexFillPaintTransformer(getVertexFillColorTransformer(profile));
		viewer.getRenderContext().setEdgeStrokeTransformer(edgeStrokeTransformer);
		viewer.getRenderContext().setEdgeDrawPaintTransformer(edgeDrawPaintTransformer);
		viewer.getRenderContext().setVertexIncludePredicate(displayVertexPredicate);
		viewer.getRenderContext().setEdgeIncludePredicate(displayEdgePredicate);
		
		if (VisualizationPreferencesFile.getDisplayStatistics().equals(VisualizationPreferencesFile.DISPLAY_STATISTICS_HOVER)) {
			viewer.setVertexToolTipTransformer(new Transformer<T, String>() {
	
				@Override
				public String transform(T vertex) {
					
					boolean isEgocenteric = vertex.equals(profile.getID());
					
					StringBuilder sb = new StringBuilder();
					
					sb.append("<html>");
					sb.append("<b>");
					sb.append(isEgocenteric ? profile.getName()
							: profile.getFriendsList().get(vertex).getName());
					
					sb.append("</b><br><hr>");
					sb.append("Degree: " + originalGraph.degree(vertex));
					
					if (profile.getAnalyzedDataExists()) {
						sb.append("<br>");
						sb.append("Clustering Coeffient: " + profile.getClusteringCoeffients().get(vertex));
						
						if (!isEgocenteric) {
							sb.append("<br>");
							sb.append("Clustering Coeffient (without egocenteric): " +
									profile.getClusteringCoeffientsWithoutRoot().get(vertex));
						}
						
						sb.append("<br>");
						sb.append("K-Shell Decomposition: " + profile.getkShellDecomposition().get(vertex));

						sb.append("<br>");
						sb.append("K-Shell Decomposition (without egocenteric): " +
									profile.getkShellDecompositionWithoutRoot().get(vertex));

						sb.append("<br>");
						sb.append("Mean Shortest Paths: " + profile.getMeanShortestPaths().get(vertex));						
					}
					
					if (!isEgocenteric) {
						sb.append("<br>");
						sb.append("Group: " + getGroupName(profile, vertex));
					}

					sb.append("</html>");

					return sb.toString();
				}
				
			});
		}
		
		ToolTipManager.sharedInstance().setDismissDelay(1000 * 60 * 5); //5 minutes maximum (should be enough for tooltip)

		viewer.addGraphMouseListener(new GraphMouseListener<T>() {

			@Override
			public void graphClicked(final T vertex, MouseEvent e) {
				//graph runs in AWT, force this to run on SWT:
				display.syncExec(new Runnable() {
					@Override
					public void run() {
						if (VisualizationPreferencesFile.getDisplayStatistics().equals(VisualizationPreferencesFile.DISPLAY_STATISTICS_CLICK)) {
							new GraphNodeDetailDialog<T>(
									tab.getParent().getShell(),
									profile, vertex,
									originalGraph.degree(vertex),
									getGroupName(profile, vertex))
										.open();
						} else {
							if (onlyClickedVertexButton.getSelection()) {
								currentVertex = vertex;
								viewer.repaint();
							}
						}									
					}
				});
			}

			@Override
			public void graphPressed(T vertex, MouseEvent e) {
				//do nothing
			}

			@Override
			public void graphReleased(T vertex, MouseEvent e) {
				//do nothing
			}
		});
		
		showClustersCombo.addListener(SWT.Selection, new Listener() {
			
			@Override
			public void handleEvent(Event e) {
				boolean withRoot = false;
				switch (showClustersCombo.getSelectionIndex()) {
					case 0:
						currentCluster = null;
						break;
					case 1:				
						withRoot = true;
					case 2:
						NumberInputDialog nid = new NumberInputDialog(parent.getShell());
						nid.setText("Cluster Calculator");
						nid.setMessageText("Please enter a number to cluster by: ");
						nid.setLimits(1, profile.getFriendsList() == null ? 1 : profile.getFriendsList().size());
						int clusterBy = nid.open();
						if (clusterBy > 0) {
							currentCluster = withRoot ?
									GraphUtils.cluster(originalGraph, clusterBy, null) :
										GraphUtils.clusterNoRoot(originalGraph, clusterBy, profile.getID(), null);
						}
				}
				viewer.repaint();
			}
		});
		
        GraphZoomScrollPane scrollablePane = new GraphZoomScrollPane(viewer);
        
        //set mouse motion sensor. Default is transforming (can change to picking):
		final DefaultModalGraphMouse<T, Integer> gm = new DefaultModalGraphMouse<T, Integer>();
		gm.setMode(ModalGraphMouse.Mode.TRANSFORMING);
		viewer.setGraphMouse(gm);
		
		modeCombo.addListener(SWT.Selection, new Listener() {
			
			@Override
			public void handleEvent(Event e) {
				if (modeCombo.getSelectionIndex() == 0) { //Transforming
					gm.setMode(ModalGraphMouse.Mode.TRANSFORMING);
				} else { //index 1, Picking
					gm.setMode(ModalGraphMouse.Mode.PICKING);
				}
			}
		});
		
		//scale back:
		ScalingControl scaler = new CrossoverScalingControl();
		scaler.scale(viewer, getHowMuchToScale(), viewer.getCenter());

		if (displayViewCombo)
			viewCombo.addListener(SWT.Selection,
					getViewComboSelectionListener(viewer));
		
		onlyClickedVertexButton.addListener(SWT.Selection, new Listener() {
			
			@Override
			public void handleEvent(Event event) {
				//handle turning off:
				if (!onlyClickedVertexButton.getSelection())
					currentVertex = null;
					viewer.repaint();
			}
		});

		closeButton.addListener(SWT.Selection, new Listener() {
			
			@Override
			public void handleEvent(Event arg0) {
				tab.dispose();
			}
		});
		
        graphFrame.add(scrollablePane);
        graphFrame.pack();
        graphFrame.setVisible(true);
		
		tab.setControl(tabComposite);
	}

}
